package org.dreamwork.tools.simulator.connector.web;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.dreamwork.injection.IObjectContext;
import org.dreamwork.tools.simulator.connector.data.DummyInterface;
import org.dreamwork.tools.simulator.connector.data.RequestLog;
import org.dreamwork.tools.simulator.connector.managed.DummyInterfaceService;
import org.dreamwork.tools.simulator.connector.managed.RequestLogService;
import org.dreamwork.util.IOUtil;
import org.dreamwork.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.time.Clock;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@WebServlet (urlPatterns = "/mock/*")
public class MockServlet extends HttpServlet {
    private final Logger logger = LoggerFactory.getLogger (MockServlet.class);
    private static final Type MAP_OF_STRING = new TypeToken<Map<String, String>> (){}.getType ();
    private DummyInterfaceService service;
    private RequestLogService rls;

    private final AtomicLong commandIdSerial = new AtomicLong (1);

    @Override
    public void init (ServletConfig config) throws ServletException {
        super.init (config);

        ServletContext app = config.getServletContext ();
        IObjectContext ctx = (IObjectContext) app.getAttribute (IObjectContext.class.getCanonicalName ());
        service = ctx.getBean (DummyInterfaceService.class);

        rls = ctx.getBean (RequestLogService.class);
    }

    @Override
    protected void service (HttpServletRequest request, HttpServletResponse response) throws IOException {
        String pathInfo = request.getPathInfo ();
        if (StringUtil.isEmpty (pathInfo)) pathInfo = "/";
        Map<String, String> values = new HashMap<> ();
        DummyInterface di = service.match (pathInfo, values);
        String method = request.getMethod ().toLowerCase (), defined = request.getMethod ();

        RequestLog log = new RequestLog ();
        log.setId (StringUtil.uuid ());
        log.setRequestTime (new Date ());
        log.setClient (getClientIp (request));
        log.setUri (pathInfo);
        log.setMethod (method);
        log.setBody (readBody (request));

        try {
            if (di == null) {
                response.setContentType ("application/json;charset=utf=8");
                response.setStatus (HttpServletResponse.SC_NOT_FOUND);
                response.getWriter ().write ("{\"code\": 404,\"message\":\"interface " + pathInfo + " not found\"}");
//            response.sendError (HttpServletResponse.SC_NOT_FOUND, "interface " + pathInfo + " not found");
            } else {
                log.setInterfaceId (di.getId ());
                if (!StringUtil.isEmpty (defined)) {
                    if (!defined.toLowerCase ().contains (method) && !"*".equals (defined)) {
                        response.sendError (
                                HttpServletResponse.SC_METHOD_NOT_ALLOWED,
                                "expect method(s): " + defined + ", but received: " + method
                        );
                        return;
                    }
                }

                String contentType = di.getType ();
                if (StringUtil.isEmpty (contentType)) {
                    contentType = "application/json;charset=utf-8";
                }
                response.setContentType (contentType);

                if (!StringUtil.isEmpty (di.getHeaders ())) {
                    Map<String, String> headers = new Gson ().fromJson (di.getHeaders (), MAP_OF_STRING);
                    headers.forEach (response::setHeader);
                }

                Integer code = di.getCode ();
                if (code == null) code = 200;
                String template = di.getValue ();
                String content = StringUtil.isEmpty (template) ? "" : generateReturnValue (di.getUri (), template, values);
                response.setStatus (code);
                response.getWriter ().write (content);
            }
        } finally {
            rls.save (log);
        }
    }

    private String generateReturnValue (String uri, String template, Map<String, String> args) {
        Gson g = new Gson ();
        Map<String, Object> map = g.fromJson (template, MAP_OF_STRING_OBJECT);

        replace (uri, map, args);
        return g.toJson (map);
    }

    private void replace (String uri, Map<String, Object> map, Map<String, String> args) {
        map.forEach ((key, value) -> {
            if (value instanceof String) {
                String text = ((String) value).trim ();
                switch (text) {
                    case "${serial}":
                        map.put (key, service.getSerial (uri));
                        break;

                    case "${commandId}":
                        map.put (key, commandIdSerial.getAndIncrement ());
                        break;

                    case "${timestamp}":
                        map.put (key, System.currentTimeMillis ());
                        break;

                    case "${datetime}":
                        map.put (key, LocalDateTime.now (Clock.systemUTC ()).toString ());
                        break;

                    case "${date}":
                        map.put (key, new SimpleDateFormat ("yyyy-MM-dd").format (System.currentTimeMillis ()));
                        break;

                    case "${imei}":
                        if (args.containsKey ("imei")) {
                            map.put (key, args.get ("imei"));
                        }
                        break;

                    case "${uuid}":
                        map.put (key, StringUtil.uuid ());
                        break;

                    default:
                        if (text.contains ("${")) {
                            StringBuffer sb = new StringBuffer ();
                            Matcher m = PATTERN.matcher (text);
                            while (m.find ()) {
                                String pattern = m.group (1), replacement = null;
                                switch (pattern) {
                                    case "imei":
                                        if (args.containsKey ("imei")) {
                                            replacement = args.get ("imei");
                                        }
                                        break;

                                    case "serial":
                                        replacement = String.valueOf (service.getSerial (uri));
                                        break;

                                    case "commandId":
                                        replacement = String.valueOf (commandIdSerial.getAndIncrement ());
                                        break;

                                    case "timestamp":
                                        replacement = String.valueOf (System.currentTimeMillis ());
                                        break;

                                    case "datetime":
                                        replacement = LocalDateTime.now ().atZone (ZoneId.of ("UTC")).toString ();
                                        break;
                                }
                                m.appendReplacement (sb, replacement == null ? pattern : replacement);
                            }
                            m.appendTail (sb);
                            map.put (key, sb.toString ());
                        }
                        break;
                }
/*
            } else if (value instanceof Number) {
                Number n = (Number) value;
                if (n.intValue () == n.doubleValue ()) {
                    map.put (key, n.intValue ());
                }
*/
            } else if (value instanceof Map) {
                @SuppressWarnings ("unchecked")
                Map<String, Object> m = (Map<String, Object>) value;
                replace (uri, m, args);
            }
        });
    }

    private String getClientIp (HttpServletRequest request) {
        String headers = request.getHeader ("x-forwarded-for");
        if (!StringUtil.isEmpty (headers)) {
            if (headers.contains (",")) {
                return headers.split (",")[0];
            } else {
                return headers;
            }
        }
        headers = request.getHeader("X-Real-IP");
        if (!StringUtil.isEmpty (headers)) {
            return headers;
        }
        return request.getRemoteAddr ();
    }

    private String readBody (HttpServletRequest request) throws IOException {
        return new String (IOUtil.read (request.getInputStream ()), StandardCharsets.UTF_8);
    }

    private final static Type MAP_OF_STRING_OBJECT = new TypeToken<Map<String, Object>> () {}.getType ();
    private final static Pattern PATTERN = Pattern.compile ("\\$\\{(.*?)}");
}